/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml.XPath;
using System.Text;
using System.Collections.Generic;

namespace DaveSexton.DocProject.DocSites
{
	public static class DocSiteNavigator
	{
		#region Public Properties
		public static XPathDocument DocSiteContentsDocument
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (docSiteContentsDocument == null)
					docSiteContentsDocument = new XPathDocument(
						HttpContext.Current.Server.MapPath(DocSiteManager.Settings.DocSiteContentsXmlSource));

				return docSiteContentsDocument;
			}
		}
		#endregion

		#region Private / Protected
		private static XPathDocument docSiteContentsDocument;

		private static string helpFileNotFoundPath;
		private static string docSiteFramePath;
		#endregion

		#region Methods
		public static void UpdateCachedSettings()
		{
			string path = DocSiteManager.Settings.HelpFileNotFoundPath;
			helpFileNotFoundPath = (VirtualPathUtility.IsAppRelative(path)) ? VirtualPathUtility.ToAbsolute(path) : path;

			path = DocSiteManager.Settings.DocSiteFramePath;
			docSiteFramePath = (VirtualPathUtility.IsAppRelative(path)) ? VirtualPathUtility.ToAbsolute(path) : path;
		}

		public static IEnumerable<string> GetSubtopics(string topic)
		{
			if (string.IsNullOrEmpty(topic))
				yield break;

			string xPath = ConvertTopicToXPath(topic, false);

			if (xPath == "//*[@name]")
				yield break;

			foreach (XPathNavigator node in GetDocSiteContentsNode(xPath).SelectChildren(XPathNodeType.Element))
			{
				// GetAttribute calls must specify an empty string for the namespaceURI argument; null does not work.
				yield return node.GetAttribute("name", "");
			}
		}

		public static void NavigateToSearchResults(string query)
		{
			if (!string.IsNullOrEmpty(query))
				HttpContext.Current.Response.Redirect("~/DocSiteSearch.aspx?q=" + HttpUtility.UrlEncode(query), true);
			else
				HttpContext.Current.Response.Redirect("~/DocSiteSearch.aspx", true);
		}

		public static void NavigateToBrowseIndex(string query)
		{
			if (!string.IsNullOrEmpty(query))
				HttpContext.Current.Response.Redirect("~/DocSiteBrowseIndex.aspx?q=" + HttpUtility.UrlEncode(query), true);
			else
				HttpContext.Current.Response.Redirect("~/DocSiteBrowseIndex.aspx", true);
		}

		/// <summary>
		/// Redirects the home page IFRAME to the specified help file if the home page is handling the current request; otherwise, redirects the user to the home page for the specified help file.
		/// </summary>
		/// <remarks>
		/// The specified help file must have a corresponding topic entry in the DocSiteContents.xml file.
		/// </remarks>
		/// <param name="helpFile">Virtual path of the help file to which the user or home page IFRAME will be redirected.</param>
		public static void NavigateToHelpFile(string helpFile)
		{
			HttpContext context = HttpContext.Current;

			if (context == null)
				throw new InvalidOperationException(Resources.Errors.NoRequestContext);

			IDocSiteDefault homePage = context.CurrentHandler as IDocSiteDefault;

			if (homePage != null)
				// redirect IFRAME
				homePage.ContentPath = helpFile ?? helpFileNotFoundPath;
			else
				context.Response.Redirect(docSiteFramePath + "?helpfile=" + helpFile, true);
		}

		/// <summary>
		/// Resolves the corresponding topic for the specified help file.
		/// </summary>
		/// <remarks>
		/// The specified help file must have a corresponding topic entry in the DocSiteContents.xml file.
		/// </remarks>
		/// <param name="urlEncode">Specifies whether each individual topic name should be url-encoded.</param>
		/// <param name="helpFile">Virtual path of the help file for which the topic will be returned.</param>
		/// <returns>The complete hierarchical path to the topic, which may be used as the <i>topic</i> argument 
		/// to the <see cref="NavigateToTopic" /> and <see cref="ResolveTopicHelpFile" /> methods.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
		public static string ResolveHelpFileTopic(string helpFile, bool urlEncode)
		{
			if (string.IsNullOrEmpty(helpFile))
				return null;

			XPathNavigator node = GetDocSiteContentsNode(
				string.Format(System.Globalization.CultureInfo.InvariantCulture, "//*[@file=\"{0}\"]", helpFile.ToLowerInvariant()));

			if (node == null)
				return null;

			StringBuilder topic = new StringBuilder();

			do
			{
				// GetAttribute calls must specify an empty string for the namespaceURI argument; null does not work.
				string name = node.GetAttribute("name", "");

				if (urlEncode)
					name = HttpUtility.UrlEncode(name);

				// do not trim name since leading and trailing spaces are valid
				topic.Insert(0, name + "/");
			}
			// NOTE: XPathNodeType.Root does not refer to the root element; just the "beginning" of the document
			while (node.MoveToParent() && node.NodeType != XPathNodeType.Root);

			if (topic.Length > 0)
				// remove trailing /
				topic.Remove(topic.Length - 1, 1);

			return topic.ToString();
		}

		/// <summary>
		/// Redirects the home page IFRAME to the specified help <paramref name="topic" /> if the home page is handling the current request; otherwise, redirects the user to the home page for the specified <paramref name="topic" />.
		/// </summary>
		/// <remarks>
		/// <para>The <paramref name="topic" /> parameter must include the names of the topics above it in its hierarchy with each topic name separated by a slash, like a file path.</para>
		/// <para>The following example illustrates the appropriate value for the <paramref name="topic" /> parameter when navigating to a topic named, "Form1 Constructor", which appears under a topic named, "Form1", which
		/// is located at the root of the help system named, "Namespaces": </para>
		/// <example>DocSiteNavigator.NavigateToTopic("Namespaces/Form1/Form1 Constructor");</example>
		/// <para>The specified topic names must correspond exactly to the name attributes of topics with the same hierarchy in the DocSiteContents.xml file.</para>
		/// </remarks>
		/// <param name="topic">Name of the topic that the user will be redirected to, including all topics in its hierarchy, with each topic separated by a slash.</param>
		public static bool NavigateToTopic(string topic, bool urlDecode)
		{
			HttpContext context = HttpContext.Current;

			if (context == null)
				throw new InvalidOperationException(Resources.Errors.NoRequestContext);

			IDocSiteDefault homePage = context.CurrentHandler as IDocSiteDefault;

			if (homePage != null)
			// redirect IFRAME
			{
				string file = ResolveTopicHelpFile(topic, urlDecode);

				if (file == null)
				// attempt to navigate to the first child topic if one exists
				{
					foreach (string subTopic in GetSubtopics(topic))
					{
						file = ResolveTopicHelpFile(topic + "/" + subTopic, urlDecode);

						if (file != null)
							break;
					}

					if (file == null)
						return false;
				}

				homePage.ContentPath = (file.Length == 0) ? helpFileNotFoundPath : file;
			}
			else
				context.Response.Redirect(GetTopicUrl(topic, true), true);

			return true;
		}

		/// <summary>
		/// Gets a virtual url that can be used to navigate to the specified <paramref name="topic" /> in a web browser.
		/// </summary>
		/// <remarks>
		/// <para>The <paramref name="topic" /> parameter must include the names of the topics above it in its hierarchy with each topic name separated by a slash, like a file path.</para>
		/// <para>The following example illustrates the appropriate value for the <paramref name="topic" /> parameter when retrieving an encoded URL to a topic named, "Form1 Constructor", which appears under a topic named, "Form1", which
		/// is located at the root of the help system named, "Namespaces": </para>
		/// <example>string topicUrl = DocSiteNavigator.GetTopicUrl("Namespaces/Form1/Form1 Constructor", true);</example>
		/// <para>The specified topic names must correspond exactly to the name attributes of topics with the same hierarchy in the DocSiteContents.xml file.</para>
		/// </remarks>
		/// <param name="topic">Name of the topic, including all topics in its hierarchy, with each topic separated by a slash.</param>
		/// <param name="urlEncode">Specifies whether each individual topic name should be url-encoded.</param>
		/// <returns>Virtual path of the specified <paramref name="topic" /> with each individual topic name, optionally, url-encoded.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")]
		public static string GetTopicUrl(string topic, bool urlEncode)
		{
			return docSiteFramePath + "?topic=" + FormatTopic(topic, urlEncode);
		}

		/// <summary>
		/// Formats the specified <paramref name="topic" /> path string by url-encoding or url-decoding each individual topic name and removing a leading and trailing / and \ character, if present.
		/// </summary>
		/// <param name="topic">Item path string to be formatted, which may include all topics in its hierarchy with each topic name separated by a slash.</param>
		/// <param name="urlEncode"><b>true</b> to have each individual topic name url-encoded; otherwise, <b>false</b> to have each individual topic name url-decoded.</param>
		/// <returns>Formatted <paramref name="topic" /> string with each individual topic name url-encoded or url-decoded, with a leading and trailing / or \ character removed.</returns>
		public static string FormatTopic(string topic, bool urlEncode)
		{
			if (string.IsNullOrEmpty(topic))
				return null;

			topic = topic.Replace('\\', '/');

			StringBuilder formatted = new StringBuilder();

			foreach (string name in topic.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
			{
				formatted.Append("/");

				if (urlEncode)
					formatted.Append(HttpUtility.UrlEncode(name));
				else
					formatted.Append(HttpUtility.UrlDecode(name));
			}

			if (formatted.Length > 0)
			{
				// remove leading /
				formatted.Remove(0, 1);

				return formatted.ToString();
			}
			else
				return null;
		}

		/// <summary>
		/// Resolves the corresponding help file for the specified <paramref name="topic" /> name.
		/// </summary>
		/// <remarks>
		/// <para>The <paramref name="topic" /> parameter must include the names of the topics above it in its hierarchy with each topic name separated by a slash, like a file path.</para>
		/// <para>The following example illustrates the appropriate value for the <paramref name="topic" /> parameter when resolving the file for a topic named, "Form1 Constructor", which appears under a topic named, "Form1", which
		/// is located at the root of the help system named, "Namespaces": </para>
		/// <example>string topicFilePath = DocSiteNavigator.ResolveTopicHelpFile("Namespaces/Form1/Form1 Constructor");</example>
		/// <para>The specified topic names must correspond exactly to the name attributes of topics with the same hierarchy in the DocSiteContents.xml file.</para>
		/// </remarks>
		/// <param name="urlDecode">Specifies whether each individual topic name should be url-decoded.</param>
		/// <param name="topic">Item name for which its virtual file path will be returned.  The value must include all topics in its hierarchy, with each topic name separated by a slash.</param>
		/// <returns>Virtual path to the help file that corresponds to the specified <paramref name="topic" />, if found in the DocSiteContents.xml file; otherwise, null.</returns>
		public static string ResolveTopicHelpFile(string topic, bool urlDecode)
		{
			XPathNavigator node = GetDocSiteContentsNode(ConvertTopicToXPath(topic, urlDecode));

			if (node == null)
				return null;
			else
			{
				// GetAttribute cannot be used because it returns an empty string regardless of whether the attribute exists
				XPathNavigator file = node.SelectSingleNode("@file");

				if (file == null)
					return null;
				else
					return file.Value ?? string.Empty;
			}
		}

		private static string ConvertTopicToXPath(string topic, bool urlDecode)
		{
			if (string.IsNullOrEmpty(topic))
				return "//*[@name]";	// default to root node

			// do not trim topic since leading and trailing spaces are valid
			topic = topic.Replace('\\', '/');

			StringBuilder xPath = new StringBuilder();

			foreach (string name in topic.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
				// do not trim name since leading and trailing spaces are valid
				xPath.AppendFormat("/*[@name=\"{0}\"]", (urlDecode) ? HttpUtility.UrlDecode(name) : name);

			if (xPath.Length > 0)
				return "/topics" + xPath.ToString();
			else
				return "//*[@name]";	// default to root node
		}

		private static XPathNavigator GetDocSiteContentsNode(string xPath)
		{
			XPathNavigator navigator = DocSiteContentsDocument.CreateNavigator();

			if (string.IsNullOrEmpty(xPath))
				// default to root node
				return navigator.SelectSingleNode("//*[@name]");
			else
				// allow null to be returned
				return navigator.SelectSingleNode(xPath);
		}
		#endregion
	}
}
